<template>
  <div class="fx-scroll-wrapper" ref="scrollWrapperDom" :style="scrollWrapperStyle" @mouseenter="handleMouseEnter"
       @mouseleave="handleMouseLeave">
    <div class="fx-scroll-content" ref="scrollContentDom" :style="scrollWrapperStyle">
      <slot/>
    </div>
    <div class="fx-scroll-bar" ref="scrollBarDom" :style="scrollBarStyle"></div>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, onBeforeUnmount, onMounted, PropType, reactive, ref, SetupContext } from 'vue'
import { codeRule } from '@/base/types'
import { addClassName, removeClassName } from '@/base/utils'

interface ScrollBarProps {
  height: number | string;
  maxHeight: number | string;
  barBgColor: string;
  barWidth: number;
  barMargin: number;
  always: boolean;
}

type DragInfo = {
  dragStart: boolean;
  dragStartY: number;
}

const SCROLL_BAR_MIN_HEIGHT = 20 // 滚动条最小高度

export default defineComponent({
  name: 'fxScrollBar',
  props: {
    // 滚动框高度
    height: {
      type: [Number, String] as PropType<number | string>,
      default: 'auto',
      validator (val): boolean {
        if (val === 'auto') return true
        if (typeof val === 'string') {
          if (Number.isNaN(Number(val))) return codeRule.test(val)
          return Number(val) > 0
        } else {
          return Number.isNaN(Number(val)) && Number(val) > 0
        }
      }
    },
    // 滚动框高度
    maxHeight: {
      type: [Number, String] as PropType<number | string>,
      default: 'auto',
      validator (val): boolean {
        if (val === 'auto') return true
        if (typeof val === 'string') {
          if (Number.isNaN(Number(val))) return codeRule.test(val)
          return Number(val) > 0
        } else {
          return Number.isNaN(Number(val)) && Number(val) > 0
        }
      }
    },
    // 滚动条背景
    barBgColor: {
      type: String as PropType<string>,
      default: 'rgba(0,0,0,0.2)'
    },
    // 滚动条宽度
    barWidth: {
      type: Number as PropType<number>,
      default: 6
    },
    // 滚动条距离边框
    barMargin: {
      type: Number as PropType<number>,
      default: 2
    },
    // 总是显示滚动条
    always: {
      type: Boolean as PropType<boolean>,
      default: false
    }
  },
  emits: ['scrollHeight', 'scroll'],
  setup (props: ScrollBarProps, ctx: SetupContext) {
    const scrollBarDom = ref<HTMLElement>()
    const scrollContentDom = ref<HTMLElement>()
    const scrollWrapperDom = ref<HTMLElement>()
    const scrollBarTop = ref<number>(0)
    const scrollBarHeight = ref<number>(0)
    const dragInfo = reactive<DragInfo>({
      dragStart: false,
      dragStartY: 0
    })

    // 滚动框样式
    const scrollWrapperStyle = computed(() => {
      const height = codeRule.test(String(props.height)) ? String(props.height) : `${props.height}px`
      const maxHeight = codeRule.test(String(props.maxHeight)) ? String(props.maxHeight) : `${props.maxHeight}px`
      return {
        height: height,
        maxHeight: maxHeight
      }
    })

    // 滚动条样式
    const scrollBarStyle = computed(() => {
      return {
        backgroundColor: props.barBgColor,
        width: `${props.barWidth}px`,
        borderRadius: `${props.barWidth / 2}px`,
        top: `${scrollBarTop.value}px`,
        right: `${props.barMargin}px`,
        height: `${scrollBarHeight.value}px`
      }
    })

    // 初始化滚动条
    const handleInitScrollBar = () => {
      if (!scrollContentDom.value || !scrollWrapperDom.value || !scrollBarDom.value) return
      const rect = scrollWrapperDom.value?.getBoundingClientRect()
      const height = scrollContentDom.value.scrollHeight - rect.height
      if (props.always) handleScroll()
      ctx.emit('scrollHeight', height < 0 ? 0 : height)
    }

    // 滚动事件
    const handleScroll = (): void => {
      if (!scrollBarDom.value || !scrollContentDom.value) return
      const rect = scrollContentDom.value?.getBoundingClientRect()
      removeClassName(scrollBarDom.value, 'leave')
      addClassName(scrollBarDom.value, 'enter')
      let barHeight = Math.floor(rect.height / scrollContentDom.value.scrollHeight * rect.height)
      barHeight = barHeight < SCROLL_BAR_MIN_HEIGHT ? SCROLL_BAR_MIN_HEIGHT : barHeight
      let disY = (scrollContentDom.value.scrollTop * (rect.height - barHeight)) / (scrollContentDom.value.scrollHeight - rect.height)
      if (disY < 0) disY = 0
      if (disY > rect.height - barHeight) disY = rect.height - barHeight
      scrollBarHeight.value = barHeight
      scrollBarTop.value = disY
      const percentage = disY / (rect.height - scrollBarDom.value.offsetHeight)
      ctx.emit('scroll', {
        scrollTop: scrollContentDom.value.scrollTop,
        scrollHeight: scrollContentDom.value.scrollHeight,
        percentage: percentage
      })
    }

    const setScrollTop = (top: number): void => {
      scrollContentDom.value && scrollContentDom.value.scrollTo(0, top)
    }

    // 拖动滚动条
    const handleScrollBarMouseMove = (ev: MouseEvent): void => {
      ev.stopPropagation()
      ev.preventDefault()
      if (!dragInfo.dragStart || !scrollContentDom.value || !scrollBarDom.value) return
      let disY = ev.clientY - dragInfo.dragStartY
      const rect = scrollContentDom.value.getBoundingClientRect()
      if (disY < 0) disY = 0
      if (disY > (rect.height - scrollBarDom.value.offsetHeight)) disY = rect.height - scrollBarDom.value.offsetHeight
      const percentage = disY / (rect.height - scrollBarDom.value.offsetHeight)
      scrollBarTop.value = disY
      setScrollTop((scrollContentDom.value.scrollHeight - rect.height) * percentage)
    }

    // 取消拖动滚动条
    const handleScrollBarMouseUp = (ev: MouseEvent): void => {
      ev.stopPropagation()
      ev.preventDefault()
      dragInfo.dragStart = false
      document.removeEventListener('mousemove', handleScrollBarMouseMove)
      document.removeEventListener('mouseup', handleScrollBarMouseUp)
    }

    // 开始拖动滚动条
    const handleScrollBarMouseDown = (ev: MouseEvent): void => {
      ev.stopPropagation()
      ev.preventDefault()
      const target = ev.target as HTMLElement
      dragInfo.dragStart = true
      dragInfo.dragStartY = ev.clientY - target.offsetTop
      document.addEventListener('mousemove', handleScrollBarMouseMove)
      document.addEventListener('mouseup', handleScrollBarMouseUp)
    }

    // 鼠标移入
    const handleMouseEnter = (): void => {
      if (!scrollBarDom.value || !scrollContentDom.value || !scrollWrapperDom.value) return
      const rect = scrollWrapperDom.value?.getBoundingClientRect()
      if (rect.height >= scrollContentDom.value.scrollHeight) return
      handleScroll()
      scrollBarDom.value.addEventListener('mousedown', handleScrollBarMouseDown)
    }

    // 鼠标移除
    const handleMouseLeave = (): void => {
      if (dragInfo.dragStart || !scrollBarDom.value) return
      scrollBarDom.value.removeEventListener('mousedown', handleScrollBarMouseDown)
      if (!props.always) {
        removeClassName(scrollBarDom.value, 'enter')
        addClassName(scrollBarDom.value, 'leave')
      }
    }

    onMounted(() => {
      handleInitScrollBar()
      scrollContentDom.value && scrollContentDom.value.addEventListener('scroll', handleScroll)
    })

    onBeforeUnmount(() => {
      scrollContentDom.value && scrollContentDom.value.removeEventListener('scroll', handleScroll)
    })

    return {
      scrollBarDom,
      scrollContentDom,
      scrollWrapperDom,
      scrollBarStyle,
      scrollWrapperStyle,
      setScrollTop,
      handleMouseEnter,
      handleMouseLeave
    }
  }
})
</script>

<style scoped lang="scss">
.fx-scroll-wrapper {
  overflow: hidden;
  position: relative;

  > .fx-scroll-content {
    width: 100%;
    height: 100%;
    position: relative;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    overflow: auto;

    &::-webkit-scrollbar {
      width: 0;
      height: 0;
      background-color: transparent;
      z-index: -1;
    }

    &::-webkit-scrollbar-track {
      -webkit-box-shadow: transparent;
      background-color: transparent;
    }

    &::-webkit-scrollbar-thumb {
      background-color: transparent;
    }
  }

  > .fx-scroll-bar {
    position: absolute;
    cursor: pointer;
    top: 0;
    transition: transform 0.3s, opacity 0.3s;

    &.enter {
      transform: translateX(0);
      opacity: 1;
    }

    &.leave {
      transform: translateX(100%);
      opacity: 0;
    }
  }
}
</style>
